-
Notifications
You must be signed in to change notification settings - Fork 2
✨ Feat: 대시보드 상세 페이지 - 터치 기반 드래그 앤 드롭 #55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
- useDragStore에서 cardId + columnId 대신 Card 객체 + columnId 저장 - extractedCard 로직 제거로 mutation 함수 단순화 - 드래그 작업에서 타입 안전성 향상 및 복잡도 감소
♻️ refactor: draggingCard에서 columnId 제거 (useDragStore.ts) —> 카드 전체 데이터 하나로 해결함(“cardData: Card”)
🎨 style: 페이지 스타일 작업 중
✨ feat: Avatar내부의 getColors 함수를 공통 함수로 분리함
🐛 fix: 드래그중인 카드가 없어도 컬럼의 배경색이 변함-->draggingCard체크 조건 추가
Walkthrough이번 변경에서는 유틸리티 함수, 스타일, 그리고 드래그 앤 드롭 기능이 대대적으로 추가 및 개선되었습니다. Sequence Diagram(s)sequenceDiagram
participant User
participant Card
participant Column
participant useDragStore
participant useCardMutation
participant API
User->>Card: 드래그 시작 (onDragStart/touch)
Card->>useDragStore: setDraggingCard(card)
User->>Column: 카드 드롭 (onDrop/touchEnd)
Column->>useDragStore: draggingCard 조회
alt 컬럼 변경 필요 시
Column->>useCardMutation: mutate({ cardId, fromColumnId, toColumnId })
useCardMutation->>API: updateCardColumn(cardId, toColumnId)
useCardMutation->>useDragStore: clearDraggingCard()
else 동일 컬럼
Column->>useDragStore: clearDraggingCard()
end
Possibly related PRs
✨ Finishing Touches
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 20
🧹 Nitpick comments (32)
src/app/shared/lib/getColor.ts (2)
1-5: 의도는 색상 ‘인덱스’ 반환이므로 함수명 혼동 가능
getColor라는 이름은 문자열 hex 값을 돌려줄 것처럼 보이지만 실제로는 배열 인덱스를 반환합니다.-export function getColor(text: string, customColors: string[]): number { +export function getColorIndex(text: string, palette: string[]): number {이후 호출부에서
palette[getColorIndex(tag, palette)]형태로 쓰면 의미가 더 명확해집니다.
1-5: 단순 합 해시 → 아나그램 충돌 위험
abc와cba가 동일 인덱스를 갖게 됩니다. UX 상 큰 문제는 아니지만, 충돌을 줄이고 싶다면 DJB2 같은 경량 해시를 쓰거나hash = ((acc << 5) - acc) + charCode패턴을 고려해 보세요..github/workflows/ci.yml (1)
11-12:self-hosted러너 사용 시 실패 대체 경로 필요
사내 러너 장애 시 워크플로 전체가 멈춥니다.runs-on: [self-hosted, ubuntu-latest]처럼 fallback 러너를 지정하거나, runner-group을 활용해 가용성을 확보해 주세요.
src/app/globals.css (1)
69-71: 드래그 Hover 색상에 다크 모드 변형을 함께 정의해 주세요.
.BG-drag-hovered클래스는 라이트 모드 전용 색상(bg-blue-100)만 적용돼 있어 다크 모드에서 시각 피드백이 사라집니다. 다른 유틸리티처럼dark:bg-[#2C3641]등을 추가해 두 테마 모두 일관된 UX를 보장해 주세요.src/app/shared/components/common/header/Collaborator/Tooltip.tsx (3)
20-30: 스크롤/리사이즈 시 위치 재계산 누락툴팁은
visible발생 시 한 번만 좌표를 계산합니다. 창 크기 변경, 스크롤 이동 후에도 툴팁이 그대로 남아 위치가 어긋날 수 있으므로window.scroll,resize이벤트에 대응해 좌표를 갱신하거나IntersectionObserver/ResizeObserver를 고려해 주세요.
40-48: 다크 모드 색상 하드코딩배경·글자색을 직접 HEX로 지정해 다크 모드 전환 시 일관성을 잃습니다. Tailwind 클래스(
BG-white,Text-black등)나 CSS 변수 사용으로 테마 의존성을 제거하는 편이 유지보수에 유리합니다.
60-68: 불필요한 래퍼 제거로 DOM 최적화 가능
<div className="relative flex items-center">는 자식이 하나뿐인 경우가 대부분이므로, 필요 없으면 Fragment로 대체해 DOM 깊이를 줄일 수 있습니다.src/app/(auth)/login/page.tsx (1)
5-10: 불필요한 Fragment 제거단일 자식만 반환하므로 빈 Fragment(<> </>)가 필요 없습니다. 간단히
<LoginForm />를 반환하도록 수정해 가독성을 높여 주세요.- return ( - <> - <LoginForm /> - </> - ) + return <LoginForm />src/app/shared/lib/cn.ts (1)
4-6:clsx인수 전달 방식 개선 제안
clsx는 가변 인자를 받도록 설계되어 있어clsx(...inputs)로 전달하는 편이 타입 추론과 가독성 모두에서 일반적입니다. 현재처럼 배열 하나를 넘겨도 동작은 하지만 타입 경고가 발생하거나 중첩 배열이 만들어질 수 있습니다.-export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(...inputs)) }src/app/shared/components/common/UserInfo.tsx (1)
11-18: 접근성 & 재사용성을 위한forwardRef적용 고려외부에서
UserInfo요소에 직접 포커스·스크롤 제어가 필요한 경우가 잦습니다(예: 메뉴 트리거).forwardRef를 적용해 DOM 참조를 노출하면 컴포넌트 활용도가 높아집니다. 한편,span대신p/div등 블록 요소가 필요할 때도 유연하게 대응할 수 있습니다.
간단히 아래와 같이 개선 가능합니다.-import { Avatar } from './Avatar' +import { forwardRef } from 'react' +import { Avatar } from './Avatar' ... -export function UserInfo({ nickname, imageUrl, size = 36 }: UserInfoProps) { - return ( +export const UserInfo = forwardRef<HTMLDivElement, UserInfoProps>( + ({ nickname, imageUrl, size = 36 }, ref) => ( <div ref={ref} className="flex items-center gap-4"> <Avatar nickname={nickname} imageUrl={imageUrl} size={size} /> <span className="text-sm font-semibold">{nickname}</span> </div> - ) -} + ), +)src/app/shared/components/common/header/UserDropdown.tsx (1)
20-28:width="w-6"값이 Dropdown 내부 스타일과 맞지 않을 가능성
w-6(24 px) 은 메뉴 아이템(텍스트 포함)에 비해 지나치게 좁아 클릭 영역이 잘리거나 overflow 가 발생할 수 있습니다. 사용 의도를 재확인하고w-48등 충분한 가로폭을 지정해 주세요.src/app/features/auth/schemas/loginValidation.ts (1)
1-16: 패스워드 복잡도 규칙 추가 고려현행 검증은 길이만 확인합니다. 서비스 정책에 따라 대·소문자, 숫자, 특수문자 포함 여부 등을 추가하면 보안 수준을 높일 수 있습니다. 필요 시 정규식 패턴과 맞춤 메시지를 포함해 주세요.
src/app/dashboard/[id]/Card/Tags.tsx (1)
6-7: 컬러 배열은 컴포넌트 외부로 이동해 재-렌더시 재생성을 방지하세요
bgColors와textColors를 함수 내부에 선언하면 렌더링마다 새 배열이 생성됩니다. 불필요한 메모리 할당을 줄이려면 파일 상단(모듈 스코프)으로 옮기는 편이 좋습니다.- const bgColors = ['#F9EEE3', '#E7F7DB', '#F7DBF0', '#DBE6F7'] - const textColors = ['#D58D49', '#86D549', '#D549B6', '#4981D5'] +const BG_COLORS = ['#F9EEE3', '#E7F7DB', '#F7DBF0', '#DBE6F7'] as const +const TEXT_COLORS = ['#D58D49', '#86D549', '#D549B6', '#4981D5'] as const + +export default function Tags({ tags }: { tags: string[] }) {src/app/features/auth/hooks/useLoginSubmit.ts (1)
17-24: 에러 메시지 타입이 불확실할 때 안전한 디폴트 추가 권장
Axios 응답message가 문자열이 아닐 가능성을 대비해typeof message === 'string'체크 후 사용하면 토스트 출력 오류를 방지할 수 있습니다.- const message = e.response?.data?.message - toast.error(message ?? '로그인 실패') + const message = e.response?.data?.message + toast.error( + typeof message === 'string' && message.length > 0 + ? message + : '로그인 실패', + )src/app/dashboard/[id]/store/useDragStore.ts (2)
4-7: 타입 명명 규칙 통일 필요
interface draggingCard와 같이 소문자로 시작하는 타입은 팀 컨벤션(PascalCase)을 벗어납니다.DraggingCard로 수정하거나 아예 별도 타입 없이Card자체를 직접 사용해도 충분합니다.-interface draggingCard { +interface DraggingCard { cardData: Card }
10-11: 파라미터 타입 중복 정의
setDraggingCard의 인자 타입을{ cardData: Card }로 직접 지정하기보다는 바로 위에서 정의한DraggingCard(또는 동일 구조)를 재사용하면 타입 중복을 줄일 수 있습니다.- setDraggingCard: (data: { cardData: Card }) => void + setDraggingCard: (data: DraggingCard) => voidsrc/app/features/auth/components/LoginForm.tsx (2)
41-44:autoComplete속성 값 재검토비밀번호 입력의
autoComplete="off"는 일부 브라우저에서 무시되며 UX 를 떨어뜨릴 수 있습니다. 일반적으로 로그인 폼에는autoComplete="current-password"를 권장합니다.- autoComplete="off" + autoComplete="current-password"
51-55: 비활성 상태 스타일 Tailwind 내장 variant 활용
disabled:variant를 사용하면 조건부 클래스를 줄일 수 있습니다.- isValid && !isSubmitting ? 'BG-blue' : 'BG-blue-disabled', + 'BG-blue disabled:BG-blue-disabled',이를 통해 로직 복잡도를 낮추고 스타일 일관성을 유지할 수 있습니다.
src/app/(auth)/signin/page.tsx (1)
57-57: 임시 입력 필드 및 페이지 정리 필요
<Input labelName="안녕" name="text" />는 실제 로그인·회원가입 흐름과 무관한 임시 코드로 보입니다. 기능 구현이 끝났다면 제거하거나 스토리북/샘플 컴포넌트로 분리해 주세요.src/app/shared/components/common/sidebar/Sidebar.tsx (2)
14-61: 목데이터 정의 위치 및 재생성 비용
mockDashboards가 컴포넌트 내부에 있어 렌더링마다 새 배열이 생성됩니다. 실제 API 연동 전이라도 상수는 컴포넌트 외부로 이동하거나useMemo로 래핑해 불필요한 재렌더링을 방지하는 편이 좋습니다.
63-66: 라우팅 시 에러 방지
router.push호출 시 현재 경로와 동일하면 불필요한 네비게이션이 발생합니다. 클릭한 대시보드가 이미 활성화되어 있을 때는 early-return 하도록 검토해 주세요.if (pathname !== `/dashboard/${dashboardId}`) { router.push(`/dashboard/${dashboardId}`) }src/app/shared/components/common/sidebar/DashboardItem.tsx (1)
36-43: 이미지 로딩 상태 처리 고려왕관 이미지에 로딩 상태나 에러 처리를 추가하면 사용자 경험이 향상될 수 있습니다.
src/app/dashboard/[id]/page.tsx (1)
32-32: 사용하지 않는 주석 코드 제거주석 처리된 코드는 제거하는 것이 좋습니다.
- // setDraggingCard({ cardData: cardData }) // 전역상태에, 현재 드래그할 카드 저장(후에 뮤테이션 함수에 전달해서 캐시 업데이트에 사용)src/app/shared/types/dashboard.ts (1)
2-10: 날짜 필드에string대신Date사용 고려
createdAt,updatedAt필드를 문자열로 두면 소비 측에서 매번 파싱 로직을 가져가야 합니다. API 스펙을 조정할 수 있다면Date타입(ISO-8601 문자열을 받아서 즉시 파싱)으로 정의하거나, 최소한 별도의type Timestamp = stringalias로 명시해 두면 의도가 더 분명해집니다.src/app/shared/components/common/Avatar.tsx (1)
13-23:customColors분리 제안
customColors상수가 컴포넌트 내부에 정의돼 있어, 다른 곳에서 같은 팔레트를 써야 할 때 중복 선언될 가능성이 있습니다.shared/constants/colors.ts같은 파일로 이동해 재사용성을 높여 주세요.src/app/dashboard/[id]/Card/Card.tsx (2)
20-24: HTML data-attribute에 대용량 JSON 직렬화 주의
data-card-data에JSON.stringify(card)를 그대로 넣으면 DOM 노드에 불필요하게 큰 문자열이 삽입됩니다. DnD 로직에서 꼭 필요하다면id만 두고, 상세 데이터는 Zustand나 context에서 조회하는 쪽이 메모리·성능 모두 유리합니다.
20-23:draggable속성은 boolean 리터럴 사용
React에서는 문자열"true"대신draggable={true}를 권장합니다.src/app/shared/components/common/header/Collaborator/CollaboratorList.tsx (1)
49-56: “+N” 툴팁 내용 부정확 가능성
visibleCollaborators[0].nickname은 이미 화면에 표시된 사람입니다. 초과 인원을 설명하려면collaborators[MAX_VISIBLE].nickname등, 실제로 숨겨진 첫 번째 사용자를 참조하는 편이 직관적입니다.src/app/shared/components/Input.tsx (1)
41-68: 접근성 속성 추가
에러 메시지가 있을 때aria-describedby를 입력 요소에 연결하면 스크린리더가 오류를 읽어줄 수 있습니다. 또한 비밀번호 토글 버튼에aria-label을 제공해 주세요.- {...rest} + aria-describedby={hasError ? `${name}-error` : undefined} + {...rest} ... - alt={showPassword ? '비밀번호 보기' : '비밀번호 숨기기'} + alt="" + aria-label={showPassword ? '비밀번호 보기' : '비밀번호 숨기기'}src/app/dashboard/[id]/Column/Column.tsx (1)
1-1: 사용되지 않는useQueryClientimport이 파일에서는
useQueryClient가 참조되지 않습니다. 불필요한 import는 제거하여 번들 크기와 린트 경고를 줄이는 것이 좋습니다.src/app/shared/components/common/Dropdown/Dropdown.tsx (2)
30-40:getWidthValue가 두 가지만 지원해 확장성이 떨어집니다지금은
w-5,w-6만 하드코딩되어 있어 새로운 폭이 필요할 때마다 코드를 수정해야 합니다.
Tailwindtheme('spacing')을 활용하거나 전달된 클래스에서 숫자를 추출해rem으로 변환하도록 일반화하면 유지보수가 쉬워집니다.
126-133: 접근성 속성 추가 제안트리거 요소에
aria-haspopup="menu"와aria-expanded={open}를 부여하면 스크린리더가 드롭다운 상태를 올바르게 알릴 수 있습니다.- <div + <div + aria-haspopup="menu" + aria-expanded={open} ref={triggerRef}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (6)
public/favicon.icois excluded by!**/*.icopublic/favicon.pngis excluded by!**/*.pngpublic/images/logo-dark.svgis excluded by!**/*.svgpublic/images/logo-light2.svgis excluded by!**/*.svgpublic/images/visibility-off.svgis excluded by!**/*.svgpublic/images/visibility-on.svgis excluded by!**/*.svg
📒 Files selected for processing (37)
.github/workflows/ci.yml(1 hunks)src/app/(auth)/login/page.tsx(1 hunks)src/app/(auth)/signin/page.tsx(1 hunks)src/app/dashboard/[id]/Card/Card.tsx(1 hunks)src/app/dashboard/[id]/Card/Tags.tsx(1 hunks)src/app/dashboard/[id]/Column/Column.tsx(3 hunks)src/app/dashboard/[id]/api/updateCardColumn.ts(1 hunks)src/app/dashboard/[id]/api/useCardMutation.ts(1 hunks)src/app/dashboard/[id]/edit/layout.tsx(1 hunks)src/app/dashboard/[id]/page.tsx(1 hunks)src/app/dashboard/[id]/store/useDragStore.ts(1 hunks)src/app/dashboard/[id]/type/Card.ts(1 hunks)src/app/features/auth/components/AuthLogo.tsx(1 hunks)src/app/features/auth/components/LoginForm.tsx(1 hunks)src/app/features/auth/hooks/useAuth.ts(2 hunks)src/app/features/auth/hooks/useLoginSubmit.ts(1 hunks)src/app/features/auth/schemas/loginValidation.ts(1 hunks)src/app/globals.css(3 hunks)src/app/layout.tsx(1 hunks)src/app/shared/components/Input.tsx(1 hunks)src/app/shared/components/common/Avatar.tsx(1 hunks)src/app/shared/components/common/CollaboratorItem.tsx(1 hunks)src/app/shared/components/common/Dropdown/Dropdown.tsx(1 hunks)src/app/shared/components/common/UserInfo.tsx(1 hunks)src/app/shared/components/common/header/Collaborator/CollaboratorList.tsx(1 hunks)src/app/shared/components/common/header/Collaborator/Tooltip.tsx(1 hunks)src/app/shared/components/common/header/Header.tsx(2 hunks)src/app/shared/components/common/header/UserDropdown.tsx(1 hunks)src/app/shared/components/common/sidebar/CreateDashboardButton.tsx(1 hunks)src/app/shared/components/common/sidebar/DashboardItem.tsx(1 hunks)src/app/shared/components/common/sidebar/Sidebar.tsx(1 hunks)src/app/shared/lib/cn.ts(1 hunks)src/app/shared/lib/getColor.ts(1 hunks)src/app/shared/types/dashboard.ts(1 hunks)src/app/tester/page.tsx(2 hunks)tailwind.config.ts(1 hunks)tsconfig.json(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (18)
src/app/dashboard/[id]/edit/layout.tsx (1)
src/app/shared/components/common/sidebar/Sidebar.tsx (1)
Sidebar(10-110)
src/app/(auth)/login/page.tsx (1)
src/app/features/auth/components/LoginForm.tsx (1)
LoginForm(11-60)
src/app/features/auth/hooks/useAuth.ts (1)
src/app/features/auth/store/useAuthStore.ts (1)
useAuthStore(5-20)
src/app/dashboard/[id]/Card/Tags.tsx (1)
src/app/shared/lib/getColor.ts (1)
getColor(1-5)
src/app/shared/components/common/UserInfo.tsx (1)
src/app/shared/components/common/Avatar.tsx (1)
Avatar(32-61)
src/app/shared/components/common/CollaboratorItem.tsx (2)
src/app/shared/lib/cn.ts (1)
cn(4-6)src/app/shared/components/common/Avatar.tsx (1)
Avatar(32-61)
src/app/features/auth/components/LoginForm.tsx (4)
src/app/features/auth/types/auth.type.ts (1)
LoginRequest(3-6)src/app/features/auth/hooks/useLoginSubmit.ts (1)
useLoginSubmit(8-28)src/app/features/auth/schemas/loginValidation.ts (1)
loginValidation(1-16)src/app/shared/lib/cn.ts (1)
cn(4-6)
src/app/shared/components/common/sidebar/CreateDashboardButton.tsx (1)
src/app/shared/types/dashboard.ts (1)
CreateDashboardButtonProps(26-28)
src/app/shared/components/common/sidebar/Sidebar.tsx (2)
src/app/shared/components/common/sidebar/CreateDashboardButton.tsx (1)
CreateDashboardButton(7-27)src/app/shared/components/common/sidebar/DashboardItem.tsx (1)
DashboardItem(7-47)
src/app/dashboard/[id]/store/useDragStore.ts (2)
src/app/dashboard/[id]/Card/Card.tsx (1)
Card(9-64)src/app/dashboard/[id]/type/Card.ts (1)
Card(6-19)
src/app/shared/components/common/sidebar/DashboardItem.tsx (1)
src/app/shared/types/dashboard.ts (1)
DashboardItemProps(20-24)
src/app/dashboard/[id]/page.tsx (5)
src/app/api/useColumns.ts (1)
useColumns(27-32)src/app/dashboard/[id]/store/useDragStore.ts (1)
useDragStore(13-17)src/app/dashboard/[id]/api/useCardMutation.ts (1)
useCardMutation(8-102)src/app/dashboard/[id]/Card/Card.tsx (1)
Card(9-64)src/app/api/useCards.ts (1)
Card(11-24)
src/app/features/auth/hooks/useLoginSubmit.ts (2)
src/app/features/auth/hooks/useAuth.ts (1)
useAuth(5-34)src/app/features/auth/types/auth.type.ts (1)
LoginRequest(3-6)
src/app/shared/components/common/header/Collaborator/CollaboratorList.tsx (2)
src/app/shared/components/common/header/Collaborator/Tooltip.tsx (1)
Tooltip(12-70)src/app/shared/components/common/CollaboratorItem.tsx (1)
CollaboratorItem(15-27)
src/app/dashboard/[id]/Card/Card.tsx (4)
src/app/api/useCards.ts (1)
Card(11-24)src/app/dashboard/[id]/store/useDragStore.ts (1)
useDragStore(13-17)src/app/dashboard/[id]/Card/Tags.tsx (1)
Tags(3-29)src/app/shared/components/common/Avatar.tsx (1)
Avatar(32-61)
src/app/shared/components/Input.tsx (1)
src/app/shared/lib/cn.ts (1)
cn(4-6)
src/app/shared/components/common/Avatar.tsx (1)
src/app/shared/lib/getColor.ts (1)
getColor(1-5)
src/app/dashboard/[id]/api/useCardMutation.ts (4)
src/app/dashboard/[id]/store/useDragStore.ts (1)
useDragStore(13-17)src/app/dashboard/[id]/type/Card.ts (1)
Card(6-19)src/app/dashboard/[id]/api/updateCardColumn.ts (1)
updateCardColumn(4-12)src/app/api/useCards.ts (1)
CardResponse(25-29)
🪛 actionlint (1.7.7)
.github/workflows/ci.yml
17-17: the runner of "actions/checkout@v3" action is too old to run on GitHub Actions. update the action's version to fix this issue
(action)
🔇 Additional comments (14)
src/app/layout.tsx (1)
11-13: 파비콘 경로 변경 후 실제 파일 존재 여부 확인 필요
.ico확장자로 변경된 것은 좋습니다만,/public/favicon.ico(또는 Next.js static 경로)에 실제 파일이 배치-되어 있지 않으면 404가 발생합니다. 배포 파이프라인에 해당 파일이 포함됐는지 한 번 더 점검해 주세요.tsconfig.json (1)
28-30: Path alias 추가는 👍 하지만 정렬·중복 여부를 체크하세요
"@hooks/*"와"@lib/*"가 새로 들어왔습니다.
- 기존 코드에서 상대경로 import 를 모두 alias 로 바꿨는지 점검해주세요.
"@components/*"→"@hooks/*"→"@lib/*"처럼 알파벳 순으로 정렬하면 diff 충돌을 줄일 수 있습니다.
[ suggest_nitpick ]src/app/globals.css (1)
53-55:Border-bottom클래스 네이밍 충돌 가능성 확인 요청Tailwind 기본
border-b유틸리티와 의미가 겹쳐 팀원 혼동 여지가 큽니다.Border-section-bottom등 프로젝트 컨벤션에 맞춘 보다 구체적인 접두사를 고려해 주세요.src/app/dashboard/[id]/type/Card.ts (1)
14-15:teamId타입 불일치 주의다른 ID 필드가 모두
number인데teamId만string입니다. 백엔드 스키마와 의도적으로 맞춘 것인지 다시 한 번 확인 바랍니다.src/app/dashboard/[id]/Card/Tags.tsx (1)
12-21:textColors[colorIndex]계산 안전성 확인 필요
getColor의 두 번째 인자로bgColors만 전달하고 있습니다. 만약textColors길이가bgColors와 달라질 경우 범위를 벗어날 수 있습니다. 두 배열 길이를 항상 동일하게 관리하거나getColor(tag, textColors)를 한 번 더 호출해 안전성을 확보하세요.src/app/shared/components/common/sidebar/CreateDashboardButton.tsx (1)
11-25: 구현 깔끔합니다!
접근성 레이블, 반응형 스타일 모두 적절합니다. 별다른 문제점이 보이지 않습니다.src/app/(auth)/signin/page.tsx (1)
11-23: 불필요 중복 폼
LoginForm컴포넌트가 이미 존재하는 상황에서MyForm이 동일 기능을 일부 중복 구현하고 있습니다. 유지보수를 위해 하나의 폼 구현으로 통합하거나 역할을 명확히 분리해 주세요.src/app/tester/page.tsx (1)
23-77: 잘 구조화된 리팩토링입니다!테스트 페이지가 애플리케이션의 일관된 레이아웃 패턴을 따르도록 Header와 Sidebar 컴포넌트를 적절히 통합했습니다.
src/app/shared/components/common/sidebar/DashboardItem.tsx (1)
1-47: 잘 구현된 컴포넌트입니다!TypeScript 타입과 접근성을 적절히 고려하여 구현되었습니다.
src/app/dashboard/[id]/api/useCardMutation.ts (1)
8-102: 낙관적 업데이트 패턴이 잘 구현되었습니다쿼리 취소, 캐시 업데이트, 에러 시 롤백 처리가 적절히 구현되었습니다.
src/app/dashboard/[id]/page.tsx (2)
66-66: 스크롤 방지 구현 계획 확인드래그 중 스크롤 방지를 적용할 계획이신가요? 적용 시 사용자가 긴 목록을 스크롤할 수 없게 되므로 신중한 고려가 필요합니다.
23-121: 터치 기반 드래그 앤 드롭이 잘 구현되었습니다롱 프레스 감지와 시각적 피드백이 적절히 구현되었습니다. 성능 최적화를 위해
requestAnimationFrame적용을 고려해보세요.src/app/shared/components/common/header/Header.tsx (1)
3-66: 깔끔한 모듈화 리팩토링입니다!헤더 컴포넌트가 더 작은 컴포넌트들로 잘 분리되어 유지보수성이 향상되었습니다.
src/app/shared/components/common/Avatar.tsx (1)
36-48:Image컴포넌트의fill사용 시크기 명시 필요 검토
fill속성을 사용할 때는 부모 컨테이너의position: relative& 고정 크기가 필수입니다. 현재 스타일로는 의도한 대로 동작하겠지만, Tailwind 클래스나 inline style 변경 시 레이아웃 붕괴 위험이 있으니 주석이나 테스트 추가를 권장합니다.
| screens: { | ||
| mobile: { max: '375px' }, | ||
| tablet: { max: '744px' }, | ||
| desktop: { max: '1920px' }, | ||
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
max breakpoint만 사용 시 의도치 않은 우선순위 충돌 가능
Tailwind 기본 sm(min 640px) 등과 조합될 때 모바일-우선이 깨질 수 있습니다.
screens: {
mobile: '375px', // min-width 기준
tablet: { max: '744px' },
}처럼 min/max 개념을 명확히 분리하거나, 모든 커스텀 스크린을 min 으로 통일하는 방안을 검토해 주세요.
🤖 Prompt for AI Agents
In tailwind.config.ts around lines 19 to 23, the current use of only max-width
breakpoints can cause unintended priority conflicts with Tailwind's default
min-width breakpoints like 'sm'. To fix this, either convert all custom screen
definitions to use min-width values consistently or clearly separate min and max
breakpoints by defining mobile as a min-width breakpoint (e.g., mobile: '375px')
while keeping others as max-width if needed. This ensures predictable and
consistent responsive behavior without priority issues.
| dueDate: string | ||
| assignee: Assignee | ||
| imageUrl: string | ||
| teamId: string | ||
| dashboardId: number | ||
| columnId: number | ||
| createdAt: string | ||
| updatedAt: string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
null 허용 여부 재검토
imageUrl, dueDate 필드는 API 응답 상 null일 가능성이 높습니다. 현재 string으로만 선언돼 있어 런타임에서 null 이 들어오면 타입 불일치 에러가 발생할 수 있습니다. 필요 시 string | null 또는 옵션(?)으로 수정해 주세요.
🤖 Prompt for AI Agents
In src/app/dashboard/[id]/type/Card.ts between lines 11 and 18, the fields
imageUrl and dueDate are currently typed as string but can be null based on API
responses. To fix this, update their types to string | null or make them
optional by adding a question mark, ensuring the type matches possible null
values and prevents runtime type errors.
| export async function updateCardColumn( | ||
| cardId: number, | ||
| columnId: number, | ||
| ): Promise<{ success: boolean }> { | ||
| const res = await axiosClient.put<{ success: boolean }>(`/cards/${cardId}`, { | ||
| columnId: columnId, | ||
| }) | ||
| return res.data |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
API 호출 실패 시 예외 처리가 누락되었습니다
네트워크 오류나 4xx/5xx 응답 시 예외가 상위로 전파되어 앱이 크래시할 수 있습니다. try-catch로 감싸고, 실패 시 적절한 에러 객체를 반환하거나 재-throw 하도록 개선해 주세요.
export async function updateCardColumn(
cardId: number,
columnId: number,
): Promise<{ success: boolean }> {
- const res = await axiosClient.put<{ success: boolean }>(`/cards/${cardId}`, {
- columnId: columnId,
- })
- return res.data
+ try {
+ const res = await axiosClient.put<{ success: boolean }>(
+ `/cards/${cardId}`,
+ { columnId },
+ )
+ return res.data
+ } catch (error) {
+ // 필요하다면 Sentry 등으로 로깅
+ throw error
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export async function updateCardColumn( | |
| cardId: number, | |
| columnId: number, | |
| ): Promise<{ success: boolean }> { | |
| const res = await axiosClient.put<{ success: boolean }>(`/cards/${cardId}`, { | |
| columnId: columnId, | |
| }) | |
| return res.data | |
| export async function updateCardColumn( | |
| cardId: number, | |
| columnId: number, | |
| ): Promise<{ success: boolean }> { | |
| try { | |
| const res = await axiosClient.put<{ success: boolean }>( | |
| `/cards/${cardId}`, | |
| { columnId }, | |
| ) | |
| return res.data | |
| } catch (error) { | |
| // 필요하다면 Sentry 등으로 로깅 | |
| throw error | |
| } | |
| } |
🤖 Prompt for AI Agents
In src/app/dashboard/[id]/api/updateCardColumn.ts around lines 4 to 11, the API
call using axiosClient.put lacks error handling, which can cause the app to
crash on network errors or 4xx/5xx responses. Wrap the axios call in a try-catch
block, and in the catch block either return a meaningful error object or
re-throw the error to ensure proper exception handling and prevent unhandled
promise rejections.
| if (!oldData) return | ||
|
|
||
| const movedCard = { ...cardData, columnId: columnId } | ||
| console.log('Cardcolumn changed', { movedCard }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
프로덕션 코드에서 console.log 제거 필요
- console.log('Cardcolumn changed', { movedCard })📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| console.log('Cardcolumn changed', { movedCard }) |
🤖 Prompt for AI Agents
In src/app/dashboard/[id]/api/useCardMutation.ts at line 72, remove the
console.log statement used for debugging to ensure no debug logs remain in the
production code.
| queryClient.setQueryData<CardResponse>( | ||
| ['columnId', columnId], | ||
| (oldData) => { | ||
| if (!oldData) return | ||
|
|
||
| const movedCard = { ...cardData, columnId: columnId } | ||
| console.log('Cardcolumn changed', { movedCard }) | ||
| return { | ||
| ...oldData, | ||
| cards: [...oldData.cards, movedCard], | ||
| } | ||
| }, | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
setQueryData 콜백에서 올바른 반환값 필요
- if (!oldData) return
+ if (!oldData) return oldData🤖 Prompt for AI Agents
In src/app/dashboard/[id]/api/useCardMutation.ts between lines 66 and 78, the
setQueryData callback does not always return a value, which is required. Modify
the callback to return the existing oldData when it is falsy instead of
returning undefined. This ensures the function consistently returns the expected
data structure.
| queryClient.setQueryData<CardResponse>( | ||
| ['columnId', currentCard.cardData.columnId], | ||
| (oldData) => { | ||
| if (!oldData) return | ||
|
|
||
| const filtered = oldData.cards.filter((card) => { | ||
| return card.id !== cardData.id | ||
| }) | ||
|
|
||
| return { ...oldData, cards: filtered } | ||
| }, | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
setQueryData 콜백에서 올바른 반환값 필요
oldData가 없을 때도 기존 데이터를 반환해야 캐시 일관성이 유지됩니다.
- if (!oldData) return
+ if (!oldData) return oldData📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| queryClient.setQueryData<CardResponse>( | |
| ['columnId', currentCard.cardData.columnId], | |
| (oldData) => { | |
| if (!oldData) return | |
| const filtered = oldData.cards.filter((card) => { | |
| return card.id !== cardData.id | |
| }) | |
| return { ...oldData, cards: filtered } | |
| }, | |
| ) | |
| queryClient.setQueryData<CardResponse>( | |
| ['columnId', currentCard.cardData.columnId], | |
| (oldData) => { | |
| if (!oldData) return oldData | |
| const filtered = oldData.cards.filter((card) => { | |
| return card.id !== cardData.id | |
| }) | |
| return { ...oldData, cards: filtered } | |
| }, | |
| ) |
🤖 Prompt for AI Agents
In src/app/dashboard/[id]/api/useCardMutation.ts around lines 53 to 64, the
setQueryData callback currently returns undefined when oldData is falsy, which
breaks cache consistency. Modify the callback to return the existing oldData
unchanged when it is falsy, ensuring the cache remains consistent by always
returning a valid data object.
| const dashboard = 15120 | ||
| const { data: columns, isLoading, error } = useColumns(dashboard) | ||
| const { draggingCard, setDraggingCard } = useDragStore() | ||
| const cardMutation = useCardMutation() | ||
| const touchPos = useRef({ x: 0, y: 0 }) | ||
| const prevColumn = useRef<HTMLElement | null>(null) | ||
| const longPressTimer = useRef<number | null>(null) | ||
| const isLongPressActive = useRef(false) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
컴포넌트 언마운트 시 타이머 정리 필요
컴포넌트가 언마운트될 때 활성 타이머를 정리하지 않으면 메모리 누수가 발생할 수 있습니다.
import { useEffect } from 'react'
// 컴포넌트 내부에 추가
useEffect(() => {
return () => {
if (longPressTimer.current) {
clearTimeout(longPressTimer.current)
}
const clone = document.getElementById('dragged-clone')
if (clone) {
clone.remove()
}
}
}, [])🤖 Prompt for AI Agents
In src/app/dashboard/[id]/page.tsx around lines 14 to 22, the longPressTimer set
with useRef is not cleared on component unmount, which can cause memory leaks.
Add a useEffect hook that returns a cleanup function to clear the longPressTimer
if it exists and remove the element with id 'dragged-clone' from the DOM when
the component unmounts.
|
|
||
| // 1. 🧱 클론 카드 제거 | ||
| const clone = document.getElementById('dragged-clone') | ||
| if (clone && isLongPressActive) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ref 값 접근 오류 수정 필요
isLongPressActive는 ref이므로 .current를 사용해야 합니다.
- if (clone && isLongPressActive) {
+ if (clone && isLongPressActive.current) {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (clone && isLongPressActive) { | |
| if (clone && isLongPressActive.current) { |
🤖 Prompt for AI Agents
In src/app/dashboard/[id]/page.tsx at line 97, the condition incorrectly
accesses the ref variable isLongPressActive directly. Since isLongPressActive is
a ref, update the condition to use isLongPressActive.current to properly access
its value.
| onDrop={(e) => { | ||
| e.preventDefault() | ||
| if (isDraggingover) setDraggingover(false) | ||
| const draggingCard = useDragStore.getState().draggingCard | ||
| if (!draggingCard) { | ||
| console.log('no dragging card') //TODO - toast 처리 🍞 | ||
| return | ||
| } | ||
|
|
||
| if (draggingCard.cardData.columnId === id) { | ||
| clearDraggingCard() | ||
| return | ||
| } | ||
| cardMutation.mutate({ | ||
| columnId: id, | ||
| cardData: draggingCard.cardData, | ||
| }) | ||
| }} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
드래그 종료 후 전역 dragging 상태가 정리되지 않습니다
mutate 호출이 완료되어도 draggingCard가 그대로 남아 있어 다음 드래그에 영향을 줄 수 있습니다.
onSettled 혹은 finally 콜백에서 clearDraggingCard()를 호출해 상태를 항상 초기화해 주세요.
cardMutation.mutate(
{
columnId: id,
cardData: draggingCard.cardData,
},
-)
+{
+ onSettled: () => clearDraggingCard(),
+})🤖 Prompt for AI Agents
In src/app/dashboard/[id]/Column/Column.tsx between lines 36 and 53, the global
dragging state is not cleared after the mutate call completes, which can affect
subsequent drag operations. To fix this, add a callback such as onSettled or
finally to the cardMutation.mutate call and invoke clearDraggingCard() there to
ensure the dragging state is always reset regardless of mutation success or
failure.
Docstrings generation was requested by @dkslel1225. * #55 (comment) The following files were modified: * `src/app/dashboard/[id]/Card/Card.tsx` * `src/app/dashboard/[id]/Card/Tags.tsx` * `src/app/dashboard/[id]/Column/Column.tsx` * `src/app/dashboard/[id]/page.tsx` * `src/app/shared/components/common/Avatar.tsx` * `src/app/shared/lib/getColor.ts`
Insung-Jo
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
터치 이벤트 수고 하셨습니다! 터치 이벤트에 대해서도 간략하게 설명해주셔서 흐름을 이해할 수 있었습니다 👍 👍
LeeCh0129
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
드래그 앤 드롭 고생하셨습니다~
상세 설명과 동영상까지 올려주셔서 어떤 플로우로 동작하는지 이해가 잘되네요 👍👍
yuj2n
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
지윤님 터치 기반 드래그앤드롭 구현 수고 많으셨습니다!!
추후 색상 함수는 재사용성을 위해 공용화하면 좋을 것 같아욤 🙂👍
| export function getColor(text: string, customColors: string[]): number { | ||
| const hash = text.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) | ||
| const index = hash % customColors.length | ||
| return index | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 부분은 공용 유틸 함수로 아래처럼 뺄 수 있을 것 같네용
export function getHashedColorIndex(text: string, colorArrayLength: number): number {
const hash = text.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)
return hash % colorArrayLength
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
앗 그러네요, 다음번 PR에서 수정하겠습니당
📌 변경 사항 개요
✨ 요약
📝 상세 내용
터치 기반 드래그 앤 드롭
터치이벤트는 전부 page.tsx에 있습니다.
1. 드래그 시작:
touchstart이벤트touchstart발생 시:touches[0].clientX,clientY) 저장2. 드래그 중:
touchmove이벤트touchmove시:4. 드래그 종료:
touchend이벤트touchend발생 시:이 외, 상세 코드에 질문 남겨주시면 답변 드리겠습니다
🎨 tailwind.config.ts - 반응형 커스텀
mobile,tablet,desktop, 설정해뒀습니다.
"사이즈: 스타일" 이렇게 작성하시면 반응형 스타일 적용됩니다.
🔗 관련 이슈
🖼️ 스크린샷
2025-06-16.3.32.07.mov
✅ 체크리스트
💡 참고 사항
Summary by CodeRabbit
신규 기능
개선 사항
버그 수정
테스트 및 문서
CI/CD 및 설정